package cs235.avl;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.JRadioButton;
import javax.swing.JCheckBox;
import javax.swing.JButton;
import javax.swing.JScrollPane;
import javax.swing.ButtonGroup;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Dimension;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;

import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;

import java.util.List;
import java.util.LinkedList;
import java.util.Set;
import java.util.TreeSet;
import java.util.Stack;

public class AvlGUI {

    public static void main(String[] params) {

        Model model = new Model();
        View view = new View(model);

    }

}

class Model {

    private Set words;
    private Tree tree;

    private Words queue;

    private Stack undo;
    private Stack redo;

    Model() {
        tree = TreeFactory.getTree();
        if (tree == null)
            System.out.println("AvlGUI: An error occured. Please make sure "
                    + "that TreeFactory.getTree() returns an object "
                    + "of type Tree instead of null");
        queue = new Words();
        words = new TreeSet();

        undo = new Stack();
        redo = new Stack();
    }

    int reportedSize() {
        return tree.size();
    }

    int size() {
        return words.size();
    }

    void clear() {
        clearTree();
        clearQueue();
        undo.clear();
        redo.clear();
    }

    BinaryTreeNode getRootNode() {
        return tree.getRootNode();
    }

    void clearQueue() {
        queue.clear();
    }

    boolean wordsEmpty() {
        return queue.isEmpty();
    }

    String getWord() {
        return queue.getWord();
    }

    String nextWord() {
        return queue.nextWord();
    }

    void loadWords(String filename) {
        queue.loadWords(filename);
    }

    void pushCopy(Stack s, Tree t, Set words) {
        t = TreeFactory.copyTree(t);
        if (t != null) {
            s.push(t);
            s.push(new TreeSet(words));
        }
    }

    void clearTree() {
        if (size() > 0)
            pushCopy(undo, tree, words);
        tree.clear();
        words.clear();
    }

    void addWord(String word, boolean doUndo) {
        if (doUndo && !words.contains(word))
            pushCopy(undo, tree, words);
        tree.add(word);
        words.add(word);
    }

    void removeWord(String word, boolean doUndo) {
        if (doUndo && words.contains(word))
            pushCopy(undo, tree, words);
        tree.remove(word);
        words.remove(word);
    }

    void addAll() {
        pushCopy(undo, tree, words);
        while (!queue.isEmpty())
            addWord(queue.nextWord(), false);
    }

    void removeAll() {
        pushCopy(undo, tree, words);
        while (!queue.isEmpty())
            removeWord(queue.nextWord(), false);
    }

    void undo() {
        redo.push(tree);
        redo.push(words);
        words = (Set) undo.pop();
        tree = (Tree) undo.pop();
    }

    void redo() {
        undo.push(tree);
        undo.push(words);
        words = (Set) redo.pop();
        tree = (Tree) redo.pop();
    }

    boolean canUndo() {
        return !undo.empty();
    }

    boolean canRedo() {
        return !redo.empty();
    }

    class Words {
        private List words;

        public Words() {
            words = new LinkedList();
        }

        public boolean isEmpty() {
            return words.isEmpty();
        }

        public void clear() {
            words.clear();
        }

        public String getWord() {
            return (String) words.get(0);
        }

        public String nextWord() {
            if (words.size() != 0)
                return (String) words.remove(0);
            else
                return "";
        }

        public void loadWords(String filename) {
            clear();
            if (filename == null)
                throw new IllegalArgumentException();
            try {
                BufferedReader file = new BufferedReader(new FileReader(filename));
                String word = file.readLine();
                while (word != null) {
                    words.add(word);
                    word = file.readLine();
                }
                file.close();
            } catch (IOException e) {
                throw new IllegalArgumentException();
            }
        }

    }
}

class View extends JFrame {

    private final Color perimeterColor = new Color(159, 205, 159);
    private final Color windowColor = new Color(204, 255, 204);

    private Model model;
    private JPanel mainPanel;
    private JPanel treePanel;
    private FilePanel filePanel;
    private WordPanel wordPanel;
    private UndoPanel undoPanel;
    private HeightPanel heightPanel;
    private JScrollPane scrollpane;

    public View(Model model) {
        super("Avl Debugger");

        this.model = model;

        mainPanel = new MainPanel();
        setContentPane(mainPanel);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        pack();
        setVisible(true);

    }

    public void updateView() {

        filePanel.setFileField("");

        if (model.wordsEmpty()) {
            wordPanel.setWordField("");
            filePanel.focusFileField();
        } else {
            wordPanel.setWordField(model.getWord());
            wordPanel.focusWordField();
        }

        wordPanel.setButtons();
        filePanel.setButtons();
        undoPanel.setButtons();
        setMessage();

        treePanel.revalidate();
        treePanel.repaint();

    }

    public void setMessage() {
        if (model.size() == model.reportedSize())
            setTitle("Avl Debugger - Tree contains " + model.size() + " words");
        else
            setTitle("Avl Debugger - Size Error - Reported size: "
                    + model.reportedSize() + ", Actual size: " + model.size());
    }

    class MainPanel extends JPanel {

        public MainPanel() {
            setLayout(new BorderLayout());
            add(new ControlPanel(), BorderLayout.NORTH);

            add(new EmptyPanel(), BorderLayout.SOUTH);
            add(new EmptyPanel(), BorderLayout.EAST);
            add(new EmptyPanel(), BorderLayout.WEST);

            treePanel = new TreePanel();
            scrollpane = new JScrollPane(treePanel);
            add(scrollpane, BorderLayout.CENTER);

        }

    }

    class EmptyPanel extends JPanel {
        public EmptyPanel() {
            setBackground(perimeterColor);
        }
    }

    class ControlPanel extends JPanel {

        public ControlPanel() {

            setBackground(perimeterColor);
            setLayout(new BorderLayout());

            JPanel north = new JPanel();
            JPanel south = new JPanel();
            north.setBackground(perimeterColor);
            south.setBackground(perimeterColor);

            filePanel = new FilePanel();
            heightPanel = new HeightPanel();
            north.add(filePanel);
            north.add(heightPanel);

            wordPanel = new WordPanel();
            undoPanel = new UndoPanel();
            south.add(wordPanel);
            south.add(undoPanel);

            add(north, BorderLayout.NORTH);
            add(south, BorderLayout.SOUTH);

        }
    }

    class WordPanel extends JPanel {

        private JLabel wordLabel;
        private JTextField wordField;

        private JRadioButton addBox;
        private JRadioButton removeBox;
        private ButtonGroup oneGroup;

        private JButton clearButton;

        void setWordField(String word) {
            wordField.setText(word);
        }

        String getWordField() {
            return wordField.getText();
        }

        void focusWordField() {
            wordField.requestFocus();
        }

        public WordPanel() {

            setBackground(perimeterColor);

            final int fieldSize = 10;

            wordLabel = new JLabel("Word: ");
            wordField = new JTextField(fieldSize);
            wordField.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    wordAction();
                }
            });

            addBox = new JRadioButton("Add Word");
            addBox.setBackground(perimeterColor);
            addBox.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    focusWordField();
                }
            });

            removeBox = new JRadioButton("Remove Word");
            removeBox.setBackground(perimeterColor);
            removeBox.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    focusWordField();
                }
            });

            oneGroup = new ButtonGroup();
            oneGroup.add(addBox);
            oneGroup.add(removeBox);
            addBox.setSelected(true);

            clearButton = new JButton("Clear Tree");
            clearButton.setMnemonic(KeyEvent.VK_C);
            clearButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    model.clearTree();
                    updateView();
                }
            });

            add(wordLabel);
            add(wordField);

            add(addBox);
            add(removeBox);

            add(clearButton);

            setButtons();

        }

        private void wordAction() {
            String word = wordField.getText();
            if (word.equals(""))
                return;

            if (addBox.isSelected())
                model.addWord(word, true);
            else
                model.removeWord(word, true);

            model.nextWord();
            updateView();
        }

        void setButtons() {
            clearButton.setEnabled(model.size() > 0);
        }

    }

    class FilePanel extends JPanel {

        private JLabel fileLabel;
        private JTextField fileField;

        private JButton addAllButton;
        private JButton removeAllButton;
        private JButton clearQueueButton;

        void setFileField(String file) {
            fileField.setText(file);
        }

        void focusFileField() {
            fileField.requestFocus();
        }

        void setButtons() {
            addAllButton.setEnabled(!model.wordsEmpty());
            removeAllButton.setEnabled(!model.wordsEmpty());
            clearQueueButton.setEnabled(!model.wordsEmpty());
        }

        public FilePanel() {

            setBackground(perimeterColor);

            final int fieldSize = 10;

            fileLabel = new JLabel("File: ");
            fileField = new JTextField(fieldSize);
            fileField.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    fileAction();
                }
            });

            addAllButton = new JButton("Add All Words");
            removeAllButton = new JButton("Remove All Words");
            clearQueueButton = new JButton("Clear Queue");

            addAllButton.setMnemonic(KeyEvent.VK_A);
            removeAllButton.setMnemonic(KeyEvent.VK_R);

            addAllButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    model.addAll();
                    updateView();
                }
            });
            removeAllButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    model.removeAll();
                    updateView();
                }
            });
            clearQueueButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    model.clearQueue();
                    updateView();
                }
            });

            add(fileLabel);
            add(fileField);

            add(addAllButton);
            add(removeAllButton);
            add(clearQueueButton);

            setButtons();
            focusFileField();

        }

        private void fileAction() {
            String file = fileField.getText();
            if (file.equals(""))
                return;

            model.loadWords(file);
            updateView();
        }

    }

    class UndoPanel extends JPanel {

        private JButton undoButton;
        private JButton redoButton;
        private JButton clearButton;

        public UndoPanel() {

            setBackground(perimeterColor);

            undoButton = new JButton("Undo");
            undoButton.setMnemonic(KeyEvent.VK_U);
            undoButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    model.undo();
                    updateView();
                }
            });

            redoButton = new JButton("Redo");
            redoButton.setMnemonic(KeyEvent.VK_D);
            redoButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    model.redo();
                    updateView();
                }
            });

            clearButton = new JButton("Clear All");
            clearButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    model.clear();
                    updateView();
                }
            });

            add(undoButton);
            add(redoButton);
            add(clearButton);

            setButtons();

        }

        void setButtons() {
            undoButton.setEnabled(model.canUndo());
            redoButton.setEnabled(model.canRedo());
            clearButton.setEnabled(model.canUndo() || model.canRedo());
        }

    }

    class HeightPanel extends JPanel {

        private JCheckBox showHeightsBox;

        public HeightPanel() {

            setBackground(perimeterColor);

            showHeightsBox = new JCheckBox("Show Heights");
            showHeightsBox.setBackground(perimeterColor);
            showHeightsBox.setSelected(true);
            showHeightsBox.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    updateView();
                }
            });

            add(showHeightsBox);

        }

        public boolean showHeights() {
            return showHeightsBox.isSelected();
        }

    }

    class TreePanel extends JPanel {

        public static final int RowHeight = 40;
        public static final int MinHeight = 8 * RowHeight;
        public static final int Margin = 20;

        public TreePanel() {
            super();
            setBackground(windowColor);
        }

        private Dimension treeSize() {

            BinaryTreeNode root = model.getRootNode();
            int height = height(root) * RowHeight;
            int width = width(root);

            return new Dimension(width, height);

        }

        public Dimension getPreferredSize() {

            int height = Math.max(MinHeight, treeSize().height);
            return new Dimension(treeSize().width + Margin * 2, height + Margin * 2);

        }

        public void paintComponent(Graphics g) {
            super.paintComponent(g);

            BinaryTreeNode root = model.getRootNode();
            int x = (getSize().width - treeSize().width) / 2;
            int y = (getSize().height - treeSize().height) / 2;

            draw(g, root, x, y);

            // draw2(g, root, getSize().width / 2, y);

            if (heightPanel.showHeights()) {

                g.drawString("Format: word (your height, correct height)", 3, 13);
                g.drawLine(0, 30, 260, 30);
                g.drawLine(260, 0, 260, 30);

                g.setColor(Color.blue);

                g.drawString("If heights don't match, the color is different.", 3, 27);

                g.setColor(Color.black);

            }
        }

        static final int BlankHeight = 8;
        static final int WordHeight = 4;
        static final int WordSpace = 4;
        static final int LineSpace = 2;

        private int height(BinaryTreeNode t) {
            if (t == null)
                return -1;
            else
                return Math.max(height(t.getLeftChild()), height(t.getRightChild())) + 1;
        }

        private int width(BinaryTreeNode t) {

            if (t == null)
                return 0;

            String word = (String) t.getData();
            if (heightPanel.showHeights())
                word += "(" + height(t) + "," + t.getHeight() + ")";
            int half = wordWidth(word) / 2 + WordSpace;

            int leftWidth = Math.max(half, width(t.getLeftChild()));
            int rightWidth = Math.max(half, width(t.getRightChild()));

            return leftWidth + rightWidth;

        }

        private int xroot;

        private int draw(Graphics g, BinaryTreeNode t, int x, int y) {

            if (t == null)
                return 0;

            String word = (String) t.getData();
            if (heightPanel.showHeights())
                word += " (" + height(t) + "," + t.getHeight() + ")";
            int half = wordWidth(word) / 2;

            int leftWidth = Math.max(half + WordSpace,
                    draw(g, t.getLeftChild(), x, y + RowHeight));
            int leftRoot = xroot;

            int rightWidth = Math.max(half + WordSpace,
                    draw(g, t.getRightChild(), x + leftWidth, y + RowHeight));
            int rightRoot = xroot;

            int width = leftWidth + rightWidth;
            // xroot = x + leftWidth;
            xroot = x + width / 2;

            if (heightPanel.showHeights() && height(t) != t.getHeight())
                g.setColor(Color.blue);

            g.drawString(word, xroot - half, y + WordHeight);

            g.setColor(Color.black);

            if (t.getLeftChild() != null)
                g.drawLine(xroot - LineSpace, y + BlankHeight, leftRoot + LineSpace, y
                        + RowHeight - BlankHeight);

            if (t.getRightChild() != null)
                g.drawLine(xroot + LineSpace, y + BlankHeight, rightRoot - LineSpace, y
                        + RowHeight - BlankHeight);

            return width;

        }

        static final int NodeWidth = 7;

        // another way to draw the tree
        public void draw2(Graphics g, BinaryTreeNode t, int x, int y) {

            if (t == null)
                return;

            String word = (String) t.getData();
            if (heightPanel.showHeights())
                word += " (" + height(t) + "," + t.getHeight() + ")";
            int half = wordWidth(word) / 2;

            int xSpace = (int) Math.pow(2, height(t) + 1) * NodeWidth;

            int leftRoot = x - xSpace;
            int rightRoot = x + xSpace;

            draw2(g, t.getLeftChild(), leftRoot, y + RowHeight);
            draw2(g, t.getRightChild(), rightRoot, y + RowHeight);

            if (heightPanel.showHeights() && height(t) != t.getHeight())
                g.setColor(Color.blue);

            g.drawString(word, x - half, y + WordHeight);

            g.setColor(Color.black);

            if (t.getLeftChild() != null)
                g.drawLine(x - LineSpace, y + BlankHeight, leftRoot + LineSpace, y
                        + RowHeight - BlankHeight);

            if (t.getRightChild() != null)
                g.drawLine(x + LineSpace, y + BlankHeight, rightRoot - LineSpace, y
                        + RowHeight - BlankHeight);

        }

        private int wordWidth(Object word) {
            return getFontMetrics(getFont()).stringWidth((String) word);
        }

    }

}
